// Copyright 2024 The PipeCD Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package planpreview

import (
	"bytes"
	"testing"

	"github.com/stretchr/testify/assert"

	"github.com/pipe-cd/pipecd/pkg/model"
)

func TestReadableResultString(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name     string
		results  []*model.PlanPreviewCommandResult
		expected string
	}{
		{
			name:    "empty",
			results: []*model.PlanPreviewCommandResult{},
			expected: `
There are no updated applications. It means no deployment will be triggered once this pull request got merged.
`,
		},
		{
			name: "there is only a plannable application",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-1",
							ApplicationName: "app-1",
							ApplicationUrl:  "https://pipecd.dev/app-1",
							ApplicationKind: model.ApplicationKind_KUBERNETES,
							Labels:          map[string]string{"env": "env-1"},
							SyncStrategy:    model.SyncStrategy_QUICK_SYNC,
							PlanSummary:     []byte("2 manifests will be added, 1 manifest will be deleted and 5 manifests will be changed"),
							PlanDetails:     []byte("changes-1"),
						},
					},
				},
			},
			expected: `
Here are plan-preview for 1 application:

1. app: app-1, env: env-1, kind: KUBERNETES
  sync strategy: QUICK_SYNC
  summary: 2 manifests will be added, 1 manifest will be deleted and 5 manifests will be changed
  details:

  ---DETAILS_BEGIN---
changes-1
  ---DETAILS_END---
`,
		},
		{
			name: "there is only a failure application",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-2",
							ApplicationName: "app-2",
							ApplicationUrl:  "https://pipecd.dev/app-2",
							ApplicationKind: model.ApplicationKind_TERRAFORM,
							Labels:          map[string]string{"env": "env-2"},
							Error:           "wrong application configuration",
						},
					},
				},
			},
			expected: `
NOTE: An error occurred while building plan-preview for the following application:

1. app: app-2, env: env-2, kind: TERRAFORM
  reason: wrong application configuration
`,
		},
		{
			name: "there is only a failure piped",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-1",
					PipedId:   "piped-1",
					PipedName: "piped-name-1",
					PipedUrl:  "https://pipecd.dev/piped-1",
					Error:     "failed to clone",
				},
			},
			expected: `
NOTE: An error occurred while building plan-preview for applications of the following Piped:

1. piped: piped-name-1 (piped-1)
  reason: failed to clone
`,
		},
		{
			name: "all kinds",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-1",
					PipedId:   "piped-1",
					PipedName: "piped-name-1",
					PipedUrl:  "https://pipecd.dev/piped-1",
					Error:     "failed to clone",
				},
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedName: "piped-name-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-1",
							ApplicationName: "app-1",
							ApplicationUrl:  "https://pipecd.dev/app-1",
							ApplicationKind: model.ApplicationKind_KUBERNETES,
							Labels:          map[string]string{"env": "env-1"},
							SyncStrategy:    model.SyncStrategy_QUICK_SYNC,
							PlanSummary:     []byte("2 manifests will be added, 1 manifest will be deleted and 5 manifests will be changed"),
							PlanDetails:     []byte("changes-1"),
						},
						{
							ApplicationId:   "app-2",
							ApplicationName: "app-2",
							ApplicationUrl:  "https://pipecd.dev/app-2",
							ApplicationKind: model.ApplicationKind_TERRAFORM,
							Labels:          map[string]string{"env": "env-2"},
							SyncStrategy:    model.SyncStrategy_PIPELINE,
							PlanSummary:     []byte("1 to add, 2 to change, 0 to destroy"),
							PlanDetails:     []byte("changes-2"),
						},
						{
							ApplicationId:   "app-3",
							ApplicationName: "app-3",
							ApplicationUrl:  "https://pipecd.dev/app-3",
							ApplicationKind: model.ApplicationKind_TERRAFORM,
							Labels:          map[string]string{"env": "env-3"},
							Error:           "wrong application configuration",
						},
						{
							ApplicationId:   "app-4",
							ApplicationName: "app-4",
							ApplicationUrl:  "https://pipecd.dev/app-4",
							ApplicationKind: model.ApplicationKind_CLOUDRUN,
							Error:           "missing key",
						},
						{
							ApplicationId:   "app-5",
							ApplicationName: "app-5",
							ApplicationUrl:  "https://pipecd.dev/app-5",
							ApplicationKind: model.ApplicationKind_ECS,
							Error:           "wrong application configuration",
						},
						{
							ApplicationId:   "app-6",
							ApplicationName: "app-6",
							ApplicationUrl:  "https://pipecd.dev/app-6",
							ApplicationKind: model.ApplicationKind_LAMBDA,
							Error:           "wrong application configuration",
						},
					},
				},
				{
					CommandId: "command-3",
					PipedId:   "piped-3",
					PipedName: "piped-name-3",
					PipedUrl:  "https://pipecd.dev/piped-3",
					Error:     "failed to checkout branch",
				},
			},
			expected: `
Here are plan-preview for 2 applications:

1. app: app-1, env: env-1, kind: KUBERNETES
  sync strategy: QUICK_SYNC
  summary: 2 manifests will be added, 1 manifest will be deleted and 5 manifests will be changed
  details:

  ---DETAILS_BEGIN---
changes-1
  ---DETAILS_END---

2. app: app-2, env: env-2, kind: TERRAFORM
  sync strategy: PIPELINE
  summary: 1 to add, 2 to change, 0 to destroy
  details:

  ---DETAILS_BEGIN---
changes-2
  ---DETAILS_END---

NOTE: An error occurred while building plan-preview for the following 4 applications:

1. app: app-3, env: env-3, kind: TERRAFORM
  reason: wrong application configuration

2. app: app-4, kind: CLOUDRUN
  reason: missing key

3. app: app-5, kind: ECS
  reason: wrong application configuration

4. app: app-6, kind: LAMBDA
  reason: wrong application configuration

NOTE: An error occurred while building plan-preview for applications of the following 2 Pipeds:

1. piped: piped-name-1 (piped-1)
  reason: failed to clone

2. piped: piped-name-3 (piped-3)
  reason: failed to checkout branch
`,
		},
		// Plugins
		{
			name: "plugin: one success app",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-1",
							ApplicationName: "app-1",
							ApplicationUrl:  "https://pipecd.dev/app-1",
							Labels:          map[string]string{"env": "env-1"},
							SyncStrategy:    model.SyncStrategy_QUICK_SYNC,
							PluginNames:     []string{"kubernetes"},
							PluginPlanResults: []*model.PluginPlanPreviewResult{
								{
									PluginName:   "kubernetes",
									DeployTarget: "dt-1",
									PlanSummary:  []byte("2 resources will be added, 1 resource will be deleted and 5 resources will be changed"),
									PlanDetails:  []byte("changes-1"),
								},
							},
						},
					},
				},
			},
			expected: `
Here are plan-preview for 1 application:

1. app: app-1, env: env-1, plan plugin(s): kubernetes
  sync strategy: QUICK_SYNC
  plugin(s): kubernetes
  summary:
  - kubernetes(dt-1): 2 resources will be added, 1 resource will be deleted and 5 resources will be changed
  details:

  - kubernetes(dt-1):
  ---DETAILS_BEGIN---
changes-1
  ---DETAILS_END---

`,
		},
		{
			name: "plugin: one success app with multiple plugins",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-1",
							ApplicationName: "app-1",
							ApplicationUrl:  "https://pipecd.dev/app-1",
							Labels:          map[string]string{"env": "env-1"},
							SyncStrategy:    model.SyncStrategy_QUICK_SYNC,
							PluginNames:     []string{"kubernetes", "terraform", "analysis"},
							PluginPlanResults: []*model.PluginPlanPreviewResult{
								{
									PluginName:   "kubernetes",
									DeployTarget: "dt-1",
									PlanSummary:  []byte("2 resources will be added, 1 resource will be deleted and 5 resources will be changed"),
									PlanDetails:  []byte("changes-1"),
								},
								{
									PluginName:   "terraform",
									DeployTarget: "dt-2",
									PlanSummary:  []byte("1 resource will be added, 2 resources will be deleted and 3 resources will be changed"),
									PlanDetails:  []byte("changes-2"),
								},
							},
						},
					},
				},
			},
			expected: `
Here are plan-preview for 1 application:

1. app: app-1, env: env-1, plan plugin(s): kubernetes, terraform
  sync strategy: QUICK_SYNC
  plugin(s): kubernetes, terraform, analysis
  summary:
  - kubernetes(dt-1): 2 resources will be added, 1 resource will be deleted and 5 resources will be changed
  - terraform(dt-2): 1 resource will be added, 2 resources will be deleted and 3 resources will be changed
  details:

  - kubernetes(dt-1):
  ---DETAILS_BEGIN---
changes-1
  ---DETAILS_END---

  - terraform(dt-2):
  ---DETAILS_BEGIN---
changes-2
  ---DETAILS_END---

`,
		},
		{
			name: "plugin: one failure app",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-2",
							ApplicationName: "app-2",
							ApplicationUrl:  "https://pipecd.dev/app-2",
							Labels:          map[string]string{"env": "env-2"},
							PluginNames:     []string{"kubernetes"},
							Error:           "wrong application configuration",
						},
					},
				},
			},
			expected: `
NOTE: An error occurred while building plan-preview for the following application:

1. app: app-2, env: env-2, plugin(s): kubernetes
  reason: wrong application configuration
`,
		},
		{
			name: "plugin: one failure app with unknown plugin",
			results: []*model.PlanPreviewCommandResult{
				{
					CommandId: "command-2",
					PipedId:   "piped-2",
					PipedUrl:  "https://pipecd.dev/piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{
							ApplicationId:   "app-2",
							ApplicationName: "app-2",
							ApplicationUrl:  "https://pipecd.dev/app-2",
							Labels:          map[string]string{"env": "env-2"},
							PluginNames:     []string{"<unknown>"},
							Error:           "wrong application configuration",
						},
					},
				},
			},
			expected: `
NOTE: An error occurred while building plan-preview for the following application:

1. app: app-2, env: env-2, plugin(s): <unknown>
  reason: wrong application configuration
`,
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			var buf bytes.Buffer
			printResults(tc.results, &buf, "")

			assert.Equal(t, tc.expected, buf.String())
		})
	}
}
func TestSortResults(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name          string
		results       []*model.PlanPreviewCommandResult
		sortLabelKeys []string
		expected      []*model.PlanPreviewCommandResult
	}{
		{
			name: "sort by pipedID and application name",
			results: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-2"},
						{ApplicationName: "app-1"},
					},
				},
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-2"},
						{ApplicationName: "app-1"},
					},
				},
			},
			sortLabelKeys: []string{},
			expected: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1"},
						{ApplicationName: "app-2"},
					},
				},
				{
					PipedId: "piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1"},
						{ApplicationName: "app-2"},
					},
				},
			},
		},
		{
			name: "sort by label keys",
			results: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
					},
				},
				{
					PipedId: "piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
						{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
						{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
						{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
					},
				},
			},
			sortLabelKeys: []string{"env"},
			expected: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "staging"}},
					},
				},
				{
					PipedId: "piped-2",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-2", Labels: map[string]string{"env": "prod"}},
						{ApplicationName: "app-3", Labels: map[string]string{"env": "prod"}},
						{ApplicationName: "app-2", Labels: map[string]string{"env": "staging"}},
						{ApplicationName: "app-3", Labels: map[string]string{"env": "staging"}},
					},
				},
			},
		},
		{
			name: "sort by multiple label keys",
			results: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
						{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
					},
				},
			},
			sortLabelKeys: []string{"env", "team"},
			expected: []*model.PlanPreviewCommandResult{
				{
					PipedId: "piped-1",
					Results: []*model.ApplicationPlanPreviewResult{
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-1"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "prod", "team": "team-2"}},
						{ApplicationName: "app-2", Labels: map[string]string{"env": "prod", "team": "team-2"}},
						{ApplicationName: "app-1", Labels: map[string]string{"env": "staging", "team": "team-1"}},
					},
				},
			},
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			sortResults(tc.results, tc.sortLabelKeys)
			assert.Equal(t, tc.expected, tc.results)
		})
	}
}

func TestToPlannedPluginNames(t *testing.T) {
	t.Parallel()
	testcases := []struct {
		name     string
		results  []*model.PluginPlanPreviewResult
		expected string
	}{
		{
			name:     "empty results",
			results:  []*model.PluginPlanPreviewResult{},
			expected: "",
		},
		{
			name: "single plugin",
			results: []*model.PluginPlanPreviewResult{
				{
					PluginName: "kubernetes",
				},
			},
			expected: "kubernetes",
		},
		{
			name: "multiple different plugins",
			results: []*model.PluginPlanPreviewResult{
				{
					PluginName: "kubernetes",
				},
				{
					PluginName: "terraform",
				},
				{
					PluginName: "cloudrun",
				},
			},
			expected: "kubernetes, terraform, cloudrun",
		},
		{
			name: "duplicate plugin names",
			results: []*model.PluginPlanPreviewResult{
				{
					PluginName: "kubernetes",
				},
				{
					PluginName: "terraform",
				},
				{
					PluginName: "kubernetes",
				},
			},
			expected: "kubernetes, terraform",
		},
	}

	for _, tc := range testcases {
		t.Run(tc.name, func(t *testing.T) {
			t.Parallel()
			got := toPlannedPluginNames(tc.results)
			assert.Equal(t, tc.expected, got)
		})
	}
}
